## Copyright 2007 Roee Shlomo, All Rights Reserved
# see LICENSE.txt for license information

import sys
import socket
from threading import Thread
from traceback import print_exc
from threading import Event
from bisect import insort
from time import sleep
from BitTornado.clock import clock
from BitTornado.natpunch import UPnP_open_port, UPnP_close_port, NATPMP_open_port, NATPMP_close_port
from defer import Deferred
from const import NUM_EVENTS

DEBUG = False

def autodetect_ipv6():
    try:
        assert sys.version_info >= (2,3)
        assert socket.has_ipv6
        socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    except:
        return 0
    return 1

class RawServer(Thread):
    """
    RawServer like UDP communicator
    # TODO: ipv6 support
    """
    def __init__(self, factory, host, port, ipv6_enable = False, upnp = 0, natpmp = False):
        Thread.__init__(self)
        if not isinstance(host,str):
            host = str(host)
        host = socket.gethostbyname(host)
        if not isinstance(port,int):
            port = int(port)
        self.addr = (host, port)
        self.factory = factory
        self.ipv6_enable = ipv6_enable
        self.doneflag = Event()
        self.funcs = []
        self.externally_added = []
        self.exccount = 0
        self.buffer = []

        # Create socket
        if self.ipv6_enable and autodetect_ipv6():
            socktype = socket.AF_INET6
        else:
            socktype = socket.AF_INET
        self.sock = socket.socket(socktype, socket.SOCK_DGRAM)
        self.sock.setblocking(0)
        self.sock.bind(self.addr)

        # UPnP / NAT-PMP
        self.port_forwarded_upnp = None
        self.port_forwarded_natpmp = None
        if upnp and UPnP_open_port(port, 'UDP'):
            self.port_forwarded_upnp = port
        elif natpmp and NATPMP_open_port(port, 'UDP', manual = True):
            self.add_task(NATPMP_open_port, 1800, [port, 'UDP'])
            self.port_forwarded_natpmp = port            
                
    def add_task(self, func, delay = 0, args = [], id = None):
        """
        Call this for adding a normal task
        """
        self.externally_added.append((func, delay, args, id))

    def add_task_prio(self, func, delay = 0, args = [], id = None):
        """
        Call this for adding an high priority task     
        """
        self.externally_added.insert(0, (func, delay, args, id))

    def _add_task(self, func, delay, args = [], id = None):
        if delay < 0:
            delay = 0
        insort(self.funcs, (clock() + delay, func, args, id))

    def pop_external(self):
        while self.externally_added:
            (a, b, c, d) = self.externally_added.pop(0)
            self._add_task(a, b, c, d)

    def exception(self):
        self.exccount += 1
        print_exc()
        
    def listen_forever(self):
        """
        Start RawServer
        """
        while not self.doneflag.isSet():
            try:
                events = 0
                # Get Functions
                self.pop_external()
                if self.doneflag.isSet():
                    return
                # Run Functions
                while self.funcs and self.funcs[0][0] <= clock():
                    garbage1, func, args, id = self.funcs.pop(0)
                    try:
                        func(*args)
                    except (SystemError, MemoryError), e:
                        print_exc()
                        return
                    except KeyboardInterrupt:
                        return
                    except:
                        if DEBUG:
                            print_exc()
                    events += 1  
                if self.doneflag.isSet():
                    return
                # Send
                events += self.flushOutgoing()
                if self.doneflag.isSet():
                    return
                # Receive
                events += self.handleIncoming()
                # Sleep
                if DEBUG:
                    print "events=", events
                if events > 0:
                    sleep(0.1)
                else:
                    sleep(0.2)
            except (SystemError, MemoryError), e:
                print_exc()
                return
            except KeyboardInterrupt:
                return            
            except:
                self.exception()                
            if self.exccount > 10:
                return
            
    def run(self):
        """
        Start RawServer
        """
        self.listen_forever()
        
    def shutdown(self):
        """
        Stop RawServer
        """
        self.doneflag.set()
        #self.sock.shutdown(2)   # ?
        self.sock.close()
        if self.port_forwarded_upnp is not None:
            UPnP_close_port(self.port_forwarded_upnp, "UDP")
        if self.port_forwarded_natpmp is not None:
            NATPMP_close_port(self.port_forwarded_natpmp, "UDP")

    ###################################################
    # Server
    ###################################################	    
    def handleIncoming(self):
        """One round of getting requests"""
        events = 0
        flag = True
        while flag and events < NUM_EVENTS:
            if self._handle_request():
                events += 1
            else:
                flag = False
        return events
    
    def _handle_request(self):
        """Handle one request, possibly blocking."""
        try:
            request, client_address = self._get_request()
        except socket.error:
            return False

        if DEBUG:
            print "in:", len(request[0])
        self._datagramReceived(request[0], client_address)
        return True
    
    def _get_request(self):
        """recieve the datagram"""
        data, client_addr = self.sock.recvfrom(8192)
        return (data, self.sock), client_addr

    def _datagramReceived(self, datagram, addr):
        """Got complete <<datagram>> from <<addr>>"""
        self.factory.krpc.datagramReceived(datagram, addr)

    ###################################################
    # Client
    ###################################################	    
    def write(self, msg, addr):
        """Add message to the queue"""
        self.buffer.append((msg, addr))

    def flushOutgoing(self):
        """One round of sending data"""
        events = 0
        n = min(len(self.buffer), NUM_EVENTS)
        for i in xrange(n):
            events += self.dispatchMsg()
        return events

    def dispatchMsg(self):
        """Send one message"""
        success = 1
        if self.buffer:
            msg, addr = self.buffer.pop(0)
            if DEBUG:
                print "out:", len(msg)
            try:    
                self.sock.sendto(msg, 0, addr)
            except socket.error:
                success = 0
        else:
            success = 0
        return success
